﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TheCopticProject.CopticCalendar
{
    public static class EasterDateCalculator
    {
        // Sunday following the full moon following the northern spring equinox => Paschal Full Moon PFM

        private const int YearsInLunarCycle = 19;
        private const int YearsInCentury = 100;
        private const int LeapYearFrequency = 4;
        private const int GregorianCenturyCycle = 4;
        private const int MaximumYearSupported = 4099;
        private const int MinimumYearSupported = 1583;

        public enum CalculationType { Gregorian, Orthodox };

        private static int GolderNumber(int year)
        {
            return year % YearsInLunarCycle;
        }

        private static int CenturyNumber(int year)
        {
            return year / YearsInCentury;
        }

        private static int DaysFromMarch21ToPaschalFullMoon(int year, CalculationType type)
        {
            if (type == CalculationType.Orthodox)
                return (YearsInLunarCycle * GolderNumber(year) + 15) % 30;
            else
            {
                int h = (CenturyNumber(year) - CenturyNumber(year) / GregorianCenturyCycle - (8 * CenturyNumber(year) + 13) / 25 + YearsInLunarCycle * GolderNumber(year) + 15) % 30; //= (23 - Epact) mod 30
                return h - (h / 28) * (1 - (h / 28) * (29 / (h + 1)) * ((21 - GolderNumber(year)) / 11));
            }
        }

        private static DayOfWeek WeekDayForPaschalFullMoon(int year, CalculationType type)
        {
            int i = DaysFromMarch21ToPaschalFullMoon(year, type);
            if (type == CalculationType.Orthodox)
                return (DayOfWeek)((year + year / LeapYearFrequency + i) % 7);
            else
                return (DayOfWeek)((year + year / LeapYearFrequency + i - DaysDifferenceBetweenJulianAndGregorianCalendar(year)) % 7);
        }

        private static int DaysDifferenceBetweenJulianAndGregorianCalendar(int year)
        {
            return CenturyNumber(year) - CenturyNumber(year) / GregorianCenturyCycle - 2;
        }

        private static int DaysFromMarch21ToEaster(int year, CalculationType type)
        {
            return DaysFromMarch21ToPaschalFullMoon(year, type) -
                (int)WeekDayForPaschalFullMoon(year, type) + (type == CalculationType.Orthodox ?
                DaysDifferenceBetweenJulianAndGregorianCalendar(year) : 0 ); //no of days from March 21 to Sunday on or before PFM (-6 to 28 methods 1 & 3, to 56 for method 2)
        }

        public static DateTime GetEasterDate(int year, CalculationType type)
        {
            if (year < MinimumYearSupported || year > MaximumYearSupported)
                throw new ArgumentException(string.Format("Gregorian calendar Easters apply for years {0} to {1} only", MinimumYearSupported, MaximumYearSupported), "year");

            /*int i = 0;   //no of days from March 21 to Paschal Full Moon
            int j = 0;   //weekday for PFM (0=Sunday, etc)
            int e = 0;   //extra days to add for method 2 (converting Julian date to Gregorian date)

            int g = year % 19; //golden year - 1
            int c = year / 100; //century

            if (type == CalculationType.Orthodox)
            {
                i = (19 * g + 15) % 30;
                j = (year + year / 4 + i) % 7;
                e = 10;
                if (year > 1600)
                    e += c - 16 - (c - 16) / 4;
            }
            else
            {
                int h = (c - c / 4 - (8 * c + 13) / 25 + 19 * g + 15) % 30; //= (23 - Epact) mod 30
                i = h - (h / 28) * (1 - (h / 28) * (29 / (h + 1)) * ((21 - g) / 11));
                j = (year + year / 4 + i + 2 - c + c / 4) % 7;
            }

            //return day and month
            int p = i - j + e; //no of days from March 21 to Sunday on or before PFM (-6 to 28 methods 1 & 3, to 56 for method 2)*/

            int daysFromMarch21 = DaysFromMarch21ToEaster(year, type);

            // p can be from -6 to 56 corresponding to dates 22 March to 23 May
            // (later dates apply to method 2, although 23 May never actually occurs)
            int day = 1 + (daysFromMarch21 + 27 + (daysFromMarch21 + 6) / 40) % 31;
            int month = 3 + (daysFromMarch21 + 26) / 30;

            return new DateTime(year, month, day);
        }
    }
}